package com.gitee.wsl.struct.attribute.pairs


import com.gitee.wsl.func.predicate.Predicate
import com.gitee.wsl.struct.attribute.pairs.AllPairs.AllPairsBuilder
import com.gitee.wsl.struct.attribute.pairs.ConstrainableCase.NoSuchParameterNameException
import com.gitee.wsl.struct.attribute.pairs.Itertools.combinations
import com.gitee.wsl.struct.attribute.pairs.Itertools.product
import kotlinx.atomicfu.atomic


/**
 * Main class containing AllPairs algorithm and providing generated test [Case]s.
 * Must be instantiated using [AllPairsBuilder].
 */
class AllPairs private constructor(allPairsBuilder: AllPairsBuilder) : Iterable<Case?> {

    val parameters: List<Parameter>

    private val constraints: List<Predicate<ConstrainableCase>>

    /** Test combination size.  */
    val testCombinationSize: Int
    private val printEachCaseDuringGeneration: Boolean
    private val combinationStorage: CombinationStorage
    /**
     * Returns expected unique `n`-wise tests combinations (considering constraints).
     *
     * @return [List] of [Map]s of `n`-wise test combinations, where key and value corresponds to
     * a [Parameter]'s name and one of its values respectively
     */
    /** Expected unique `n`-wise test combinations (considering constraints).  */
    val expectedUniqueTestCombinations: List<Map<String, Any>>

    /** Generated unique `n`-wise test combinations.  */
    /**
     * Returns generated unique `n`-wise tests combinations.
     *
     * @return [List] of [Map]s of `n`-wise test combinations, where key and value corresponds to
     * a [Parameter]'s name and one of its values respectively
     */
    var generatedUniqueTestCombinations: List<Map<String, Any?>>? = null
        get() {
            if (field == null) {
                field = findGeneratedUniqueTestCombinations()
            }
            return field
        }
        private set

    private val itemMatrix: List<MutableList<Item>>

    /**
     * Returns generated test [Case]s.
     *
     * @return [List] of [Case]s
     */
    val generatedCases: List<Case>

    init {
        parameters = allPairsBuilder.parameters
        constraints = allPairsBuilder.constraints
        testCombinationSize = allPairsBuilder.n
        printEachCaseDuringGeneration = allPairsBuilder.printEachCaseDuringGeneration
        combinationStorage = CombinationStorage(testCombinationSize)
        itemMatrix = createItemMatrix(parameters)
        expectedUniqueTestCombinations = findExpectedUniqueTestCombinations()
        generatedCases = generateCases()
    }

    /**
     * The entry point for users.
     *
     * @see AllPairsBuilder.withParameter
     * @see AllPairsBuilder.withConstraint
     * @see AllPairsBuilder.withTestCombinationSize
     */
    class AllPairsBuilder {
        val parameters: MutableList<Parameter> = ArrayList()
        val constraints: MutableList<Predicate<ConstrainableCase>> = ArrayList()
        var n = 2
        var printEachCaseDuringGeneration = false

        /**
         * Adds one [Parameter].
         *
         *  * Must provide at least two [Parameter]s
         *  * Each [Parameter] must have at least one value
         *  * Each [Parameter] must have no duplicate values
         *  * [Parameter] `name` must be unique
         *
         * **Examples:**
         * <pre>`new Parameter("OS", "Windows", "Linux", "macOS")
         * new Parameter("RAM", 2048, 4096, 8192, 16384)
        `</pre> *
         *
         * @param parameter [Parameter]
         * @return a reference to [AllPairsBuilder] object
         */
        fun withParameter(parameter: Parameter): AllPairsBuilder {
            //Objects.requireNonNull(parameter, "Parameter must be non-null")
            parameters.add(parameter)
            return this
        }

        /**
         * Adds [List] of [Parameter]s.
         *
         *
         * For detailed description see [AllPairsBuilder.withParameter].
         *
         * @param parameters [List] of [Parameter]s
         * @return a reference to [AllPairsBuilder] object
         * @see AllPairsBuilder.withParameter
         */
        fun withParameters(parameters: List<Parameter>): AllPairsBuilder {
            //Objects.requireNonNull(parameters, "Parameters must be non-null")
            this.parameters.addAll(parameters)
            return this
        }

        /**
         * Adds one test [Case] constraint as [Predicate]. Each potential test [Case] is tested
         * against it. If test evaluates to `true`, the [Case] under test won't be present in the result
         * and the algorithm will search for another [Case], so that in the end all possible `n`-wise test
         * combinations are covered (considering all constraints).
         *
         *  *  If not specified, all possible test combinations will be generated
         *  *  If non-existing [Parameter] name is provided, it will always evaluate to `false`
         *
         * **Examples:**
         * <pre>`// Cases with "Foo" and "Bar" pair won't be generated:
         * c -> c.get("paramName1").equals("Foo") && c.get("paramName2").equals("Bar")
         * // Cases with "paramName"'s value greater than 5 won't be generated:
         * c -> c.get("paramName") > 5
        `</pre> *
         *
         *
         * **Recommended usage:**
         *
         *
         * Try to simplify constraints as much as possible. Too complicated constraints might cause longer algorithm
         * processing time, especially on a large input of [Parameter]s. For example, consider two different
         * constraints for the following input:
         * <pre>`Browser: "Chrome"
         * OS:      "Linux", "macOS"
         * Drive:   "HDD", "SSD"
         *
         * // complicatedConstraint (filter out 'Chrome-Linux-HDD' combination):
         * c -> c.get("Browser").equals("Chrome") && c.get("OS").equals("Linux") && c.get("Drive").equals("HDD")
         * // simplifiedConstraint (filter out 'Linux-HDD' pair):
         * c -> c.get("OS").equals("Linux") && c.get("Drive").equals("HDD")
        `</pre> *
         * It is better to use `simplifiedConstraint`, because the usage of `complicatedConstraint`
         * implies there might be pairs including non-`'Chrome'` browsers, while in fact there is only one
         * possible browser.
         *
         * @param constraint [Predicate]`<ConstrainableCase>` to filter out unwanted [Case]s
         * @return a reference to [AllPairsBuilder] object
         * @see ConstrainableCase
         *
         * @see AllPairsBuilder.withParameter
         */
        fun withConstraint(constraint: Predicate<ConstrainableCase>): AllPairsBuilder {
            constraints.add(constraint)
            return this
        }

        /**
         * Adds [List] of [Case] constraints to filter `n`-wise test combinations.
         *
         *
         * For detailed description see [AllPairsBuilder.withConstraint].
         *
         * @param constraints [List] of [Predicate]s to filter out unwanted [Case]s
         * @return a reference to [AllPairsBuilder] object
         * @see AllPairsBuilder.withConstraint
         */
        fun withConstraints(constraints: List<Predicate<ConstrainableCase>>): AllPairsBuilder {
            this.constraints.addAll(constraints)
            return this
        }

        /**
         * Specifies test combination length. 2 - pairwise, 3 - triplewise, etc.
         *
         *  * Must be greater than or equal to 2
         *  * Must be less than or equal to the number of [Parameter]s
         *  * If not specified, the default value 2 will be used
         *
         *
         * @param n length of n-wise test combination
         * @return a reference to [AllPairsBuilder] object
         * @see AllPairsBuilder.withParameter
         */
        fun withTestCombinationSize(n: Int): AllPairsBuilder {
            this.n = n
            return this
        }

        /**
         * Specifies that each [Case] should be printed during generation.
         * It could be useful for debug or to identify problems when generation takes too long.
         *
         *
         * If not specified, printing will be disabled.
         *
         * @return a reference to [AllPairsBuilder] object
         */
        fun printEachCaseDuringGeneration(): AllPairsBuilder {
            printEachCaseDuringGeneration = true
            return this
        }

        /**
         * Using provided configuration, builds a new instance of [AllPairs] with generated test [Case]s.
         *
         * @return [AllPairs] instance
         */
        fun build(): AllPairs {
            validate()
            return AllPairs(this)
        }

        private fun validate() {
            require(n >= 2) { "Minimum test combination size is 2. Provided: $n" }
            if (parameters.size < n) {
                throw IllegalArgumentException("The number of Parameters (${parameters.size}) must be greater than ot equal to the test combination size ($n)")
            }
            parameters.forEach { parameter: Parameter ->
                if (parameter.isEmpty()) {
                    throw IllegalArgumentException(
                        ("Each Parameter must have at least one value. " + "Provided Parameter with no values: " + parameter.name)
                    )
                }
                if (parameter.name == null) {
                    throw IllegalArgumentException("Parameter name must be non-null. Provided: $parameter")
                }
                if (parameter.name.isEmpty()) {
                    throw IllegalArgumentException("Parameter name must not be empty. Provided: $parameter")
                }
                if (parameter.distinct().count() != parameter.size) {
                    throw IllegalArgumentException("Each Parameter must have no duplicate values. Provided: $parameter")
                }
            }
            val nonUniqueParameterNamesFiller: MutableSet<String> = HashSet()
            val nonUniqueParameterNames = parameters
                .map(Parameter::name)
                .filter{ name: String -> !nonUniqueParameterNamesFiller.add(name) }

            if (nonUniqueParameterNames.isNotEmpty()) {
                throw IllegalArgumentException("Parameter name must be unique. Provided non-unique names: $nonUniqueParameterNames")
            }
        }
    }

    fun getConstraints(): List<Predicate<ConstrainableCase>> {
        return constraints
    }

    override fun iterator(): Iterator<Case> {
        return generatedCases.iterator()
    }

    override fun toString(): String {
        return if (generatedCases != null) {
            val index = atomic(1)
            generatedCases.joinToString("/") { c: Case ->
                "${index.getAndIncrement()}:$c"
                //String.format("%3d: %s", index.getAndIncrement(), c.toString())
            }
        } else {
            "Cases are not generated yet"
        }
    }

    private fun generateCases(): List<Case> {
        val cases: MutableList<Case> = ArrayList()
        var caseCount = 0
        while (true) {
            val nextCase = generateNextCase() ?: break
            if (printEachCaseDuringGeneration) {
                //System.out.printf("%3d: %s%n", ++caseCount, nextCase)
                "${++caseCount}:$nextCase"
            }
            cases.add(nextCase)
        }
        return cases
    }

    /**
     * Generates next test [Case] using AllPairs algorithm.
     *
     * @return [Case] or `null` if all [Case]s are already found
     */
    private fun generateNextCase(): Case? {
        if (combinationStorage.length > expectedUniqueTestCombinations.size) {
            throw RuntimeException("Actual number of test combinations exceeded possible maximum")
        }
        if (combinationStorage.length == expectedUniqueTestCombinations.size) {
            return null // All test combinations are found
        }
        val previousUniqueTestCombinationsCount = combinationStorage.length
        val chosenItems: MutableList<Item?> = ArrayList()
        val itemIndexes: MutableList<Int?> = ArrayList()
        for (i in itemMatrix.indices) {
            chosenItems.add(null)
            itemIndexes.add(null)
        }
        var direction = 1
        var i = 0 // Item group index
        while (i > -1 && i < itemMatrix.size) {
            if (direction == 1) {
                updateWeightsAndReSortItemMatrix(chosenItems.subList(0, i) as List<Item>, i)
                itemIndexes[i] = 0
            } else {
                itemIndexes[i] = itemIndexes[i]!! + 1
                if (itemIndexes[i]!! >= itemMatrix[i].size) {
                    direction = -1
                    if (i == 0) {
                        return null // Can't find more new test combinations after all values brute force
                    }
                    i += direction
                    continue
                }
            }
            chosenItems[i] = itemMatrix[i][(itemIndexes[i])!!]
            direction = if (constraints == null || constraints.isEmpty() || isValidCase(chosenItems.subList(0, i + 1))) {
                1
            } else {
                0
            }
            i += direction
            if (i == itemMatrix.size) {
                combinationStorage.addSequenceCombinations(chosenItems as List<Item>)
                // Chosen items didn't produce new test combinations
                if (combinationStorage.length == previousUniqueTestCombinationsCount) {
                    direction = -1
                    i += direction
                }
            }
        }
        return Case(chosenItems)
    }

    private fun updateWeightsAndReSortItemMatrix(chosenItems: List<Item>, itemGroupIndex: Int) {
        for (item in itemMatrix[itemGroupIndex]) {
            val node = combinationStorage.getNodeOrCreateNew(item)
            val newItemIdCombinations: MutableList<Set<List<String>>> = mutableListOf()
            for (i in 0 until testCombinationSize) {
                val items: MutableList<Item> = ArrayList(chosenItems)
                items.add(item)
               /* val newItemIdCombination = StreamSupport
                    .stream(combinations(items, i + 1).spliterator(), false)
                    .map({ itemCombination: List<Item?> ->
                        itemCombination.stream()
                            .map<String>(Item::itemId)
                            .collect(Collectors.toList())
                    })
                    .collect(Collectors.toSet())*/
                val newItemIdCombination = combinations(items, i + 1)
                    .map{itemCombination: List<Item> ->
                        itemCombination.map(Item::itemId)
                    }.toMutableSet()

                newItemIdCombination.removeAll(combinationStorage.itemIdCombinations[i])
                newItemIdCombinations.add(newItemIdCombination)
            }
            val weights: MutableList<Int> = ArrayList()
            // Node that creates most new test combinations is the best
            weights.add(-newItemIdCombinations[newItemIdCombinations.size - 1].size)
            // Less used outbound connections are most likely to produce more test combinations
            weights.add(node.outboundItemIdsSize)
            if (newItemIdCombinations.size >= 2) {
                for (i in newItemIdCombinations.size - 2 downTo 0) {
                    weights.add(newItemIdCombinations[i].size)
                }
            }
            weights.add(node.counter) // Less used node is better
            weights.add(-node.inboundItemIdsSize) // Prefer node with most free inbound connections
            item.weights = weights
        }
        itemMatrix[itemGroupIndex].sort()
    }

    private fun createItemMatrix(parameters: List<Parameter>): List<MutableList<Item>> {
        val matrix: MutableList<MutableList<Item>> = ArrayList()
        for ((i, parameter) in parameters.withIndex()) {
            matrix.add(ArrayList())
            for (j in parameter.indices) {
                matrix[i].add(
                    Item("a${i}v$j", parameter[j]!!, parameter.name)
                )
            }
        }
        return matrix
    }

    /**
     * Tests all constraints.
     *
     * @param items [Item]s [List] representing possible [Case] to test constraint against
     * @return `false` if met at least one constraint, `true` if met no constraints
     */
    private fun isValidCase(items: List<Item?>): Boolean {
        if (constraints.isEmpty()) {
            return true
        }
        val constrainableCase = ConstrainableCase(items)
        for (constraint in constraints) {
            try {
                if (constraint.test(constrainableCase)) {
                    return false
                }
            } catch (ignored: NoSuchParameterNameException) {
                // NoSuchKeyInCaseException is used for program flow to allow Constraints to work as Predicates
                // NoSuchKeyInCaseException is optimized to cause as little performance penalty as possible
            }
        }
        return true
    }

    private fun findExpectedUniqueTestCombinations(): List<Map<String, Any>> {
        /*return StreamSupport
            .stream(combinations(itemMatrix, testCombinationSize).spliterator(), false)
            .flatMap({ itemsComb: List<MutableList<Item?>>? ->
                StreamSupport.stream(
                    product(itemsComb)
                        .spliterator(),
                    false
                )
            })
            .filter({ items: List<Item?> ->
                isValidCase(
                    items
                )
            })
            .map({ items: List<Item?> ->
                items.stream().collect(
                    Collectors.toMap(
                        Item::name,
                        Item::value,
                        BinaryOperator { key1: Any?, key2: Any? -> key1 },
                        Supplier { LinkedHashMap() })
                )
            })
            .collect(Collectors.toList())*/


        return  combinations(itemMatrix, testCombinationSize)
            .flatMap{ itemsComb: List<MutableList<Item>> ->
                product(itemsComb)
            }
            .filter{ items: List<Item> ->
                isValidCase(items)
            }
            .map{ items: List<Item> ->
                val map = mutableMapOf<String, Any>()
                items.forEach {
                    map[it.name] = it.value
                }
               map
            }.toList()

    }

    private fun findGeneratedUniqueTestCombinations(): List<Map<String, Any?>> {
        /*return generatedCases!!.stream()
            .map({ aCase: Case ->
                aCase.entries.stream()
                    .map(Function<Map.Entry<String, Any?>, Map<String, Any?>> { entry: Map.Entry<String, Any?> ->
                        val parameter: MutableMap<String, Any?> =
                            HashMap()
                        parameter.put(entry.key, entry.value)
                        parameter
                    })
                    .collect(
                        Collectors.toList()
                    )
            })
            .flatMap(
                Function<List<Map<String, Any?>>, Stream<out List<MutableMap<String, Any?>>>> { caseAsMapPerValue: List<Map<String, Any?>>? ->
                    StreamSupport.stream(
                        combinations(
                            caseAsMapPerValue,
                            testCombinationSize
                        ).spliterator(),
                        false
                    )
                })
            .distinct()
            .map<Map<String, Any?>>({ testCombinationAsMapPerValue: List<MutableMap<String, Any?>> ->
                testCombinationAsMapPerValue.stream()
                    .reduce(
                        LinkedHashMap(),
                        BinaryOperator { testCombination: MutableMap<String, Any?>, value: Map<String, Any?>? ->
                            testCombination.putAll(
                                (value)!!
                            )
                            testCombination
                        })
            })
            .collect(Collectors.toList())*/

        return generatedCases
            .map{ aCase: Case ->
                aCase.entries
                    .map{ entry ->
                        val parameter: MutableMap<String, Any?> = HashMap()
                        parameter[entry.key] = entry.value
                        parameter
                    }
            }.flatMap{caseAsMapPerValue ->
                   /* StreamSupport.stream(
                        combinations(
                            caseAsMapPerValue,
                            testCombinationSize
                        ).spliterator(),
                        false
                    )*/
                combinations(caseAsMapPerValue, testCombinationSize)
                }
            .distinct()
            .map{ testCombinationAsMapPerValue: List<MutableMap<String, Any?>> ->
                val testCombination= mutableMapOf<String, Any?>()
                testCombinationAsMapPerValue.forEach {
                    testCombination.putAll(it)
                }
                testCombination
            }.toList()

    }
}

